/**
 * @license
 * Copyright 2021 Du Tian Wei
 * SPDX-License-Identifier: Apache-2.0
 */

let OpenBlock = {
    I18N: {},
    wsBuildCbs: [],
    connectors: [],
    currentConnector: null,
    defaultConfirm(msg, callback) {
        if (confirm(msg)) {
            callback(true);
        } else {
            callback(false);
        }
    },
    i(msg) {
        if (!window.Blockly) {
            console.log('尚未初始化Blockly');
            return msg;
        }
        if (!Blockly.Msg) {
            console.log('尚未初始化Blockly.Msg');
            return msg;
        }
        let bm = Blockly.Msg[msg];
        if (bm) {
            return bm;
        } else {
            return msg;
        }
    }
};
OpenBlock.InitWorkspace = (ws) => {
    OpenBlock.wsBuildCbs.forEach((cb) => {
        cb(ws);
    });
};
/**
 * 
 * @param {OBConnector} connector 
 */
OpenBlock.addConnector = (connector) => {
    OpenBlock.connectors.push(connector);
    if (OpenBlock.connectors.length === 1) {
        OpenBlock.useConnector(connector);
    }
};
OpenBlock.useConnector = (connector) => {
    if (connector) {
        OpenBlock.clearNativeInfo();
        OpenBlock.currentConnector = connector;
        connector.loadConfig();
    }
};
OpenBlock.clearNativeInfo = () => {
    OpenBlock.initNativeFunctionAndTypes();
    OpenBlock.clearNativeBlock();
};
OpenBlock.loadJS = (srcs, cb) => {
    function loadScript() {
        if (srcs.length === 0) {
            if (cb) {
                cb();
                return;
            }
        }
        let src = srcs.shift();
        if (src) {
            let script = document.createElement('script');
            script.src = src;
            script.type = 'text/javascript';
            script.onload = loadScript;
            script.onerror = (e) => {
                console.error(e);
                loadScript();
            }
            document.head.appendChild(script);
            //loadScript();
        }
    }
    loadScript();
};

OpenBlock.init = (conf) => {
    let scripts = document.getElementsByTagName('script');
    for (let script of scripts) {
        if (script.src.endsWith('OpenBlock.js')) {
            let src = script.src;
            OpenBlock.bootPath = src.substring(0, src.length - 12);
            break;
        }
    }
    if (conf.uiCallbacks) {
        conf.uiCallbacks = Object.assign({
            confirm: OpenBlock.defaultConfirm
        }, conf.uiCallbacks);
    }
    conf = Object.assign({
        extI18NPath: OpenBlock.bootPath + '/i18n/',
        logicToolbox: null,
        lang: null,
    }, conf);
    if (!conf.toolbox) {
        throw new Error('toolbox required');
    }
    if (typeof (conf.toolbox.state) !== 'string') {
        throw new Error('state toolbox must be string');
    }
    if (typeof (conf.toolbox.function) !== 'string') {
        throw new Error('function toolbox must be string');
    }
    OpenBlock.config = conf;
    OpenBlock.language = (OpenBlock.config.lang || new URLSearchParams(location.search).get('lang') || navigator.language || navigator.browserLanguage).toLowerCase();

    let srcs = [
        OpenBlock.bootPath + '../3rd/blockly/blocks_compressed.js',
        OpenBlock.bootPath + '../3rd/blockly-plugins/field-grid-dropdown.js',
        OpenBlock.bootPath + '../3rd/blockly-plugins/zoom-to-fit.js',
        OpenBlock.bootPath + '../3rd/blockly-plugins/workspace-search.js',
        OpenBlock.bootPath + '../3rd/blockly-plugins/modal.js',
        OpenBlock.bootPath + '../3rd/easysax.js',
        OpenBlock.bootPath + '../3rd/sheetjs/xlsx.full.min.js',
        OpenBlock.bootPath + 'utils.js',
        OpenBlock.bootPath + 'dag.js',
        OpenBlock.bootPath + 'ubdropdown.js',
        OpenBlock.bootPath + 'typed_procedures.js',
        OpenBlock.bootPath + 'typetree.js',
        OpenBlock.bootPath + 'UBBCblocks.js',
        OpenBlock.bootPath + 'struct.js',
        OpenBlock.bootPath + 'compiler/WorkerContext.js',
        OpenBlock.bootPath + 'compiler/AST/SObject/Serializable.js',
        OpenBlock.bootPath + 'compiler/AST/ModuleAST.js',
        OpenBlock.bootPath + 'compiler/compiler.js',
        OpenBlock.bootPath + 'compiler/parsers/blocklyfsm.js',
        OpenBlock.bootPath + 'compiler/parsers/blocklyparser.js',
        OpenBlock.bootPath + 'compiler/parsers/blocks.js',
        OpenBlock.bootPath + 'compiler/linker.js',
        OpenBlock.bootPath + 'compiler/datastructure.js',
        OpenBlock.bootPath + 'nativeFunction.js',
        OpenBlock.bootPath + 'compiler/buildinFunctions.js',
        OpenBlock.bootPath + 'stub_toolbox.js',
        OpenBlock.bootPath + 'asyncparser.js',
        OpenBlock.bootPath + 'dataImporter.js',
        OpenBlock.bootPath + 'nativeBlock.js',
        OpenBlock.bootPath + 'NativeTypeChecker.js',
        OpenBlock.bootPath + 'compiler/analyses/stateData.js',
        OpenBlock.bootPath + 'toolboxSearch.js',
        OpenBlock.bootPath + 'collect.js',
        OpenBlock.bootPath + 'debugger/connector.js',
    ];

    function addI18n(path) {
        // let s = document.head.appendChild(document.createElement('script'))
        // s.src = './js/i18n/' + path + '.js'
        if (srcs.indexOf(path) >= 0) {
            return;
        }
        if (OpenBlock.config.extI18NPath) {
            srcs.unshift(OpenBlock.config.extI18NPath + path + '.js');
        }
        srcs.unshift(OpenBlock.bootPath + 'i18n/' + path + '.js');
        srcs.unshift(OpenBlock.bootPath + '../3rd/blockly/msg/js/' + path + '.js');
    }
    addI18n('zh-hans');
    if (OpenBlock.language !== 'zh-hans' && OpenBlock.language !== 'zh-cn') {
        let subIndex = OpenBlock.language.indexOf('-');
        if (subIndex > 0) {
            let fallbackLang = OpenBlock.language.substring(0, subIndex);
            addI18n(fallbackLang);
        }
        addI18n(OpenBlock.language);
    }
    // blockly 应该在最先，比i18n先
    srcs.unshift(OpenBlock.bootPath + '../3rd/blockly/blockly_compressed.js');
    OpenBlock.loadJS(srcs, OpenBlock._onInited);
};

OpenBlock.initedCbs = [];

OpenBlock.onInited = function (f) {
    OpenBlock.initedCbs.push(f);
};
OpenBlock.configBlockly = function () {
    Blockly.ContextMenuRegistry.registry.unregister('blockDisable');
    /**
    * Clean up the workspace by ordering all the blocks in a column.
    */
    Blockly.WorkspaceSvg.prototype.cleanUp = function () {
        this.setResizesEnabled(false);
        Blockly.Events.setGroup(true);
        var topBlocks = this.getTopBlocks(true);
        var cursorX = 0;
        for (var i = 0, block; (block = topBlocks[i]); i++) {
            if (!block.isMovable()) {
                continue;
            }
            var xy = block.getRelativeToSurfaceXY();
            block.moveBy(cursorX - xy.x, - xy.y);
            block.snapToGrid();
            cursorX = block.getRelativeToSurfaceXY().x +
                block.getHeightWidth().width +
                this.renderer_.getConstants().MIN_BLOCK_WIDTH;
        }
        Blockly.Events.setGroup(false);
        this.setResizesEnabled(true);
    };
};
OpenBlock._onInited = function () {
    // config blockly
    OpenBlock.configBlockly();
    //
    OpenBlock.initedCbs.forEach(cb => {
        cb();
    });
};



/**
 * opt:
 * {
 * fsms:[] optional,
 * structs:[] optional,
 * }
 */
OpenBlock.newSrc = (opt) => {
    if (typeof (opt.env) === 'string') {
        opt.env = [opt.env];
    }
    opt = Object.assign(OpenBlock.DataStructure.createModuleTemplate(), opt);

    if (!opt.name) {
        /**
         * @type {Array}
         */
        let arr = OpenBlock.BlocklyParser.loadedFiles.srcs.concat(OpenBlock.BlocklyParser.loadedFiles.libs);
        if (arr.find(i => {
            return i.name === OpenBlock.I18N.START_SRC_NAME;
        })) {
            opt.name = OpenBlock.Utils.genName(OpenBlock.I18N.NEW_SRC_NAME, arr);
        } else {
            opt.name = OpenBlock.Utils.genName(OpenBlock.I18N.START_SRC_NAME, arr);
        }
    }
    OpenBlock.BlocklyParser.analyze();
    return opt;
};

OpenBlock.changeSrcName = (src, name) => {
    if (OpenBlock.getSrcByName(name)) {
        return false;
    }
    if (OpenBlock.BlocklyParser.loadedFiles.dependingTree.changeNodeValue(src.name, name)) {
        src.name = name;
    }
};

OpenBlock.hasFSM = (src, name) => {
    return OpenBlock.Utils.hasName(src.fsms, name);
};
OpenBlock.hasState = (fsm, name) => {
    return OpenBlock.Utils.hasName(fsm.states, name);
};
OpenBlock.hasFunction = (src, name) => {
    return OpenBlock.Utils.hasName(src.functions, name);
};
/**
 * opt:
 * {
 * name:string optional,
 * code:string optional,
 * }
 */


OpenBlock.addFSM = (src, opt) => {
    opt = Object.assign(OpenBlock.DataStructure.createFSMTemplate(), opt);
    if (!opt.name) {
        if (src.name === OpenBlock.I18N.START_SRC_NAME && !src.fsms.find(i => i.name === OpenBlock.I18N.START_FSM_NAME)) {
            opt.name = OpenBlock.Utils.genName(OpenBlock.I18N.START_FSM_NAME, src.fsms);
        } else {
            opt.name = OpenBlock.Utils.genName(OpenBlock.I18N.NEW_FSM_TYPE_NAME, src.fsms);
        }
    }
    src.fsms.push(opt);
    OpenBlock.addState(opt);
    return opt;
};
OpenBlock.addState = (fsm, opt) => {
    opt = Object.assign(OpenBlock.DataStructure.createStateTemplate(), opt);
    if (!opt.name) {
        opt.name = OpenBlock.Utils.genName(OpenBlock.I18N.NEW_STATE_NAME, fsm.states);
    }
    fsm.states.push(opt);
    OpenBlock.BlocklyParser.analyze();
    return opt;
};

OpenBlock.removeFSMVariable = (fsm, v) => {
    for (let i = 0; i < fsm.variables.length; i++) {
        if (fsm.variables[i] === v) {
            OpenBlock.config.uiCallbacks.confirm('删除 ' + v.name, (r) => {
                if (r) {
                    fsm.variables.splice(i, 1);
                    OpenBlock.exportExePackage();
                }
            });
        }
    }
};
OpenBlock.removeFSMVariableBySN = (fsm, sn) => {
    for (let i = 0; i < fsm.variables.length; i++) {
        if (fsm.variables[i].sn === sn) {
            OpenBlock.config.uiCallbacks.confirm('删除 ' + fsm.variables[i].name, (r) => {
                if (r) {
                    fsm.variables.splice(i, 1);
                    OpenBlock.exportExePackage();
                }
            });
        }
    }
};
OpenBlock.removeFSMVariableByName = (fsm, name) => {
    for (let i = 0; i < fsm.variables.length; i++) {
        if (fsm.variables[i].name === name) {
            OpenBlock.config.uiCallbacks.confirm('删除 ' + fsm.variables[i].name, (r) => {
                if (r) {
                    fsm.variables.splice(i, 1);
                    OpenBlock.exportExePackage();
                }
            });
        }
    }
};
OpenBlock.removeStateVariableByName = (state, name) => {
    for (let i = 0; i < state.variables.length; i++) {
        if (state.variables[i].name === name) {
            OpenBlock.config.uiCallbacks.confirm('删除 ' + state.variables[i].name, (r) => {
                if (r) {
                    state.variables.splice(i, 1);
                    OpenBlock.exportExePackage();
                }
            });
        }
    }
}
/**
 * 
 * @param {fsm} fsm 
 * @param {object} info { name[string], type[string], wrap[list/map/null], export[bool]}
 */
OpenBlock.addFSMVariable = (fsm, info) => {
    let name = info.name;
    for (let i in fsm.variables) {
        let v = fsm.variables[i];
        if (v.name === name) {
            throw new Error("NAME_DUPLICATED");
        }
    }
    let type = info.type;
    if (info.wrap) {
        type = info.wrap + '<' + type + '>';
    }
    let vinfo = { name, type: type, export: !!info.export };
    fsm.variables.push(vinfo);
    OpenBlock.BlocklyParser.analyze();
};
OpenBlock.addStateVariable = (state, info) => {
    let name = info.name;
    for (let i in state.variables) {
        let v = state.variables[i];
        if (v.name === name) {
            throw new Error("NAME_DUPLICATED");
        }
    }
    let type = info.type;
    if (info.wrap) {
        type = info.wrap + '<' + type + '>';
    }
    let vinfo = { name, type: type, export: !!info.export };
    state.variables.push(vinfo);
    OpenBlock.BlocklyParser.analyze();
};

OpenBlock.addFunction = (src, opt) => {
    opt = Object.assign(OpenBlock.DataStructure.createFunctionTemplate(), opt);
    if (!opt.name) {
        opt.name = OpenBlock.Utils.genName(OpenBlock.I18N.NEW_FUNCTION_NAME, src.functions);
    }
    src.functions.push(opt);
    OpenBlock.BlocklyParser.analyze();
    return opt;
};

OpenBlock.getSrcByName = (name) => {
    for (let src of OpenBlock.BlocklyParser.loadedFiles.srcs) {
        if (src.name === name) {
            return src;
        }
    }
    return null;
};

OpenBlock.getLibByName = (name) => {
    for (let src of OpenBlock.BlocklyParser.loadedFiles.libs) {
        if (src.name === name) {
            return src;
        }
    }
    return null;
};

OpenBlock.getFsmByName = (src, name) => {
    if (!src) {
        return null;
    }
    for (let fsm of src.fsms) {
        if (fsm.name === name) {
            return fsm;
        }
    }
    return null;
};

OpenBlock.getStateByName = (fsm, name) => {
    if (!fsm) {
        return null;
    }
    for (let state of fsm.states) {
        if (state.name === name) {
            return state;
        }
    }
    return null;
};

OpenBlock.getFuncByName = (src, name) => {
    if (!src) {
        return null;
    }
    for (let func of src.functions) {
        if (func.name === name) {
            return func;
        }
    }
    return null;
};

OpenBlock._buildBlockly = (domContainer, xml, code, env) => {
    let opt = Object.assign({
        plugins: {
            [Blockly.registry.Type.CONNECTION_CHECKER]: "NativeTypeChecker"
        }
    }, OpenBlock.config.blocklyOpt);
    opt.media = OpenBlock.bootPath + '../3rd/blockly/media/';
    opt.toolbox = xml;
    let workspace = Blockly.inject(domContainer, opt);
    const zoomToFit = new ZoomToFitControl(workspace);
    zoomToFit.init();
    const workspaceSearch = new WorkspaceSearch(workspace);
    workspaceSearch.init();
    // workspaceSearch.open();
    workspace.setResizesEnabled(true);
    workspace._openblock_env = env;
    OpenBlock.InitWorkspace(workspace)
    // workspace.addChangeListener(Blockly.Events.disableOrphans);
    // onresize();
    Blockly.svgResize(workspace);
    if (code && code.startsWith('<xml')) {
        Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(code), workspace);
    }
    return workspace;
};

OpenBlock._initStateToolbox = (xmlDom) => {
    OpenBlock.injectNativeCategory(xmlDom);
    if (OpenBlock.config.stubToolbox) {
        OpenBlock.StubToolbox.injectCategory(xmlDom);
    }
};

OpenBlock._initFunctionToolbox = (xmlDom) => {
    OpenBlock.injectNativeCategory(xmlDom);
    if (OpenBlock.config.stubToolbox) {
        OpenBlock.StubToolbox.injectCategory(xmlDom);
    }
};

OpenBlock.buildStateBlockly = (src, fsm, state, domContainer) => {
    if (!domContainer) {
        throw new Error('domContainer is null');
    }
    if (src.fsms.indexOf(fsm) < 0) {
        throw new Error('fsm not in src');
    }
    if (fsm.states.indexOf(state) < 0) {
        throw new Error('state not in fsm');
    }
    domContainer.innerHTML = "";

    let xmlDom = Blockly.Xml.textToDom(OpenBlock.config.toolbox.state);
    OpenBlock._initStateToolbox(xmlDom);
    let workspace = OpenBlock._buildBlockly(domContainer, xmlDom, state.code, {
        _openblock_src: src,
        _openblock_state: state,
        _openblock_fsm: fsm,
        _openblock_type: 'state',
        _openblock_target: state
    });
    let ret = {
        context: {
            src: src,
            fsm: fsm,
            state: state,
            type: 'state',
            workspace
        },
        value: state,
        resize: function () {
            // workspace.resize();
            Blockly.svgResize(workspace);
        },
        dispose: function () {
            workspace.dispose();
        },
        saveCode: function () {
            state.code = Blockly.Xml.domToText(Blockly.Xml.workspaceToDom(workspace));
        }
    };
    ret.context.workspace.saveCode = ret.saveCode;
    workspace.clearUndo();
    return ret;
};

OpenBlock.buildFunctionBlockly = (src, func, domContainer) => {
    if (!domContainer) {
        throw new Error('domContainer is null');
    }
    if (src.functions.indexOf(func) < 0) {
        throw new Error('function not in source');
    }
    domContainer.innerHTML = "";

    let xmlDom = Blockly.Xml.textToDom(OpenBlock.config.toolbox.function);
    OpenBlock._initFunctionToolbox(xmlDom);
    let workspace = OpenBlock._buildBlockly(domContainer, xmlDom, func.code, {
        _openblock_src: src,
        _openblock_function: func,
        _openblock_type: 'function',
        _openblock_target: func
    });
    let ret = {
        context: {
            src: src,
            function: func,
            type: 'function',
            workspace
        },
        value: func,
        resize: function () {
            // workspace.resize();
            Blockly.svgResize(workspace);
        },
        dispose: function () {
            workspace.dispose();
        },
        saveCode: function () {
            func.code = Blockly.Xml.domToText(Blockly.Xml.workspaceToDom(workspace));
        }
    };
    ret.context.workspace.saveCode = ret.saveCode;
    workspace.clearUndo();
    return ret;
};
OpenBlock.buildStateBlocklyByName = (srcName, fsmName, stateName, domContainer) => {
    let src = OpenBlock.getSrcByName(srcName);
    let fsm = OpenBlock.getFsmByName(src, fsmName);
    let state = OpenBlock.getStateByName(fsm, stateName);

    return OpenBlock.buildStateBlockly(src, fsm, state, domContainer);
};

OpenBlock.buildFunctionBlocklyByName = (srcName, funcName, domContainer) => {
    let src = OpenBlock.getSrcByName(srcName);
    let func = OpenBlock.getFuncByName(src, funcName);

    return OpenBlock.buildFunctionBlockly(src, func, domContainer);
};

OpenBlock.hiddenClear = function (k, v) {
    if (!k.startsWith('_')) {
        return v;
    }
}
/**
 * 将指定的源文件序列化为字符串格式
 */
OpenBlock.serializeSrc = (src, cb) => {
    cb(null, JSON.stringify(src, OpenBlock.hiddenClear));
};
/**
 * 将指定的源文件编译为库(blob)
 */
OpenBlock.serializeLib = (src, cb) => {
    if (!src.__analyzed) {
        OpenBlock.BlocklyParser.analyze();
    }
    if (src._errors && src._errors.length > 0) {
        cb(src._errors);
        return;
    }
    OpenBlock.Compiler.compile(src.__analyzed, true, cb);

};
OpenBlock.exportExePackage = function (cb) {
    OpenBlock.BlocklyParser.analyze();
    let waiting = 0;
    let errors = [];
    let srcs = OpenBlock.BlocklyParser.loadedFiles.srcs;
    function link() {
        for (let i = 0; i < srcs.length; i++) {
            let src = srcs[i];
            if (src.errors && src.errors.length > 0) {
                errors = errors.concat(src.errors);
            }
            if (src.__compiled && src.__compiled.errors && src.__compiled.errors.length > 0) {
                errors = errors.concat(src.__compiled.errors);
            }
        }
        if (errors.length > 0) {
            if (cb) {
                cb(errors)
            }
            return;
        }
        let compiledarr = [];
        OpenBlock.BlocklyParser.loadedFiles.libs.forEach(l => {
            compiledarr.push(l);
        });
        OpenBlock.BlocklyParser.loadedFiles.srcs.forEach(l => {
            compiledarr.push(l.__compiled);
        });
        OpenBlock.DataImporter.reorganizeData();
        OpenBlock.Linker.link(compiledarr, OpenBlock.DataImporter.importedData,
            Object.assign({}, OpenBlock.nativefunctions.custom, { 'buildin': OpenBlock.nativefunctions.buildin }),
            cb);
    }
    for (let i = 0; i < srcs.length; i++) {
        let src = srcs[i];
        // if (src._errors && src._errors.length > 0) {
        //     if (cb) {
        //         cb(src._errors);
        //     }
        //     return;
        // }
        if (true || !src.__compiled) {
            waiting++;
            OpenBlock.Compiler.compile(src.__analyzed, false, (err, compiled) => {
                if (err) {
                    src._errors.push(err);
                    errors.push(err);
                } else {
                    src.__compiled = compiled;
                }
                waiting--;
                if (waiting === 0) {
                    setTimeout(link, 1);
                }
            });
        }
    }
    if (waiting === 0) {
        link();
    }
};
OpenBlock.setAllBlockStyles = (styleArray) => {
    for (let themeName in Blockly.Themes) {
        let theme = Blockly.Themes[themeName]
        for (let k in styleArray) {
            theme.blockStyles[k] = styleArray[k];
        }
    }
};

OpenBlock.setCategoryStyle = (cateName, style) => {
    for (let themeName in Blockly.Themes) {
        let theme = Blockly.Themes[themeName]
        theme.categoryStyles[cateName] = style;
    }
};

OpenBlock.getRelatedSrcOrLib = (src) => {
    let srcArr = [src];
    src.depends.forEach(srcName => {
        let src1 = OpenBlock.getSrcByName(srcName);
        if (src1) {
            srcArr.push(src1);
        } else {
            let lib = OpenBlock.getLibByName(srcName);
            if (lib) {
                srcArr.push(lib);
            } else {
                throw "找不到依赖项：" + srcName;
            }
        }
    });
    return srcArr;
};
OpenBlock.getAvailableTypes = function (src) {
    let types = [].concat(OpenBlock.I18N.PRIMARY_TYPES);
    let libs = OpenBlock.getRelatedSrcOrLib(src);

    libs.forEach(lib => {
        // structArr = structArr.concat(rSrc.structs);
        lib.__analyzed.structs.forEach(st => {
            let fullname = lib.name + "." + st.name;
            types.push([fullname, fullname]);
        });
    });
    return types;
}

/**
 * 
 * @param {string[]} jsarr 国际化文件、存根文件等
 * @param {string} toolboxpath 描述本地库展示项的xml文件
 * @param {*} callback 
 */
OpenBlock.loadNativeInfo = function (jsarr, toolboxpath, callback) {
    OpenBlock.loadJS(jsarr, () => {

        $.ajax({
            type: 'GET',
            url: toolboxpath,
            dataType: 'text',
            success: function (data) {
                OpenBlock.NativeBlocks(data);
                if (callback) {
                    callback();
                }
                OpenBlock.exportExePackage();
            },
            error: function (e) {
                console.warn(e);
                if (callback) {
                    callback();
                } else {
                    throw e;
                }
            }
        });
    });
}